"""Provides some of the classes and logic for handling configuration data."""


import os
from abc import ABCMeta
from typing import Literal, Optional, Generic, TypeVar, Union, ClassVar, TypeAlias
from contextlib import AbstractContextManager

import attrs
import yaml
import arcpy

from .iterablenamespace import IterableNamespace, FrozenDict
from .misc import ArcFieldTypeKeyword, ArcFieldTypeStr, FIELD_TYPE_KEYWORDS

CodedValueDict: TypeAlias = dict[str, str]
DomainType: TypeAlias = Literal["CODED", "RANGE"]
DomainInfoNamespace: TypeAlias = IterableNamespace[Union[str, CodedValueDict]]
FieldInfoNamespace: TypeAlias = IterableNamespace[Union[str, int, DomainInfoNamespace, None]]
FeatureClassInfoNamespace: TypeAlias = IterableNamespace[Union[str, list[str]]]


class YAMLIterableNamespaceMeta(ABCMeta, yaml.YAMLObjectMetaclass):
    """Empty class that serves to prevent a metaclass conflict in
    :class:`_NG911Config`."""
    pass


class WDManager(AbstractContextManager[None]):
    """Context manager that temporarily changes the working directory."""

    target_directory: Optional[str]
    original_directory: str

    def __init__(self, directory: Optional[str] = None):
        self.target_directory = directory
        self.original_directory = os.getcwd()

    def __enter__(self):
        super().__enter__()
        if self.target_directory:
            os.chdir(self.target_directory)

    def __exit__(self, __exc_type, __exc_value, __traceback):
        os.chdir(self.original_directory)


class ConfigDumper(yaml.Dumper):
    """YAML dumper that can generate ``config.yml``."""

    anchored_scalars: ClassVar[dict[Union[str, int, float, bool, None], str]] = {}
    """Registry for scalar values that should be assigned specifically-named
    anchors. The keys are the scalars and the values are the anchor names."""

    @property
    def serialized_scalar_values(self) -> list[Union[str, int, float, bool, None]]:
        """Returns a list of all scalar values that have been serialized (so
        far) by an instance."""
        return [n.value for n in self.serialized_nodes if isinstance(n, yaml.ScalarNode)]

    @classmethod
    def anchor_scalar(cls, value: Union[str, int, float, bool, None], alias: str):
        """Registers a scalar value that should be assigned an anchor named
        *alias* in the output YAML."""
        cls.anchored_scalars[value] = alias

    def generate_anchor(self, node):
        if isinstance(node, yaml.ScalarNode) and node.value in self.anchored_scalars:
            alias = self.anchored_scalars[node.value]
        elif node.tag == "!Domain":
            alias = {k.value: v.value for k, v in node.value}["name"]  # Name of the domain
            pass
        elif node.tag == "!Field":
            alias = {k.value: v.value for k, v in node.value}["role"]  # Role of the field
        else:
            alias = super().generate_anchor(node)
        if alias in self.anchors.values():
            raise ValueError(f"Alias '{alias}' already in use.")
        return alias

    def represent_data(self, data):
        if isinstance(data, IterableNamespace) and not hasattr(data, "yaml_tag"):
            return self.represent_dict(dict(data))
            # return self.yaml_representers[dict](self, dict(data))
        else:
            return super().represent_data(data)

    def serialize_node(self, node, parent, index):
        # Contains some code duplicated and/or adapted from serializer.py
        if isinstance(node, yaml.ScalarNode) \
                and node.value in self.anchored_scalars \
                and (node.value is None or isinstance(node.value, (str, int, float, bool))):
            alias = self.anchored_scalars[node.value]
            if node.value in self.serialized_scalar_values:
                self.emit(yaml.AliasEvent(alias))
            else:
                self.serialized_nodes[node] = True
                self.descend_resolver(parent, index)
                detected_tag = self.resolve(yaml.ScalarNode, node.value, (True, False))
                default_tag = self.resolve(yaml.ScalarNode, node.value, (False, True))
                implicit = (node.tag == detected_tag), (node.tag == default_tag)
                self.emit(yaml.ScalarEvent(alias, node.tag, implicit, node.value, style=node.style))
                self.ascend_resolver()
        else:
            return super().serialize_node(node, parent, index)


@attrs.frozen(repr=False, str=False)
class NG911Domain(yaml.YAMLObject):
    yaml_tag = "!Domain"
    yaml_loader = yaml.SafeLoader
    yaml_dumper = ConfigDumper

    name: str
    description: str
    type: DomainType
    entries: FrozenDict[str, str] = attrs.field(converter=FrozenDict)

    @property
    def as_namespace(self) -> DomainInfoNamespace:
        return IterableNamespace(
            name=self.name,
            description=self.description,
            type=self.type,
            entries=self.entries
        )

    @classmethod
    def from_yaml(cls, loader: yaml.SafeLoader, node: yaml.Node):
        # Explicitly writing this method was necessary when changing from @dataclass to @attrs.frozen
        assert isinstance(node, yaml.MappingNode)
        mapping = loader.construct_mapping(node, deep=True)
        return cls(**mapping)

    def __repr__(self):
        num_entries = len(self.entries)
        entry_word = "Entry" if num_entries == 1 else "Entries"
        return f"<{__class__.__name__} '{self.name}' [{num_entries} {entry_word}]>"

    def __format__(self, format_spec: str) -> str:
        """
        The following custom format specifiers are implemented:

        - ``n`` - Returns the :attr:`name` attribute
        - ``d`` - Returns the :attr:`description` attribute
        """
        # noinspection PyUnreachableCode
        match format_spec:
            case "n":
                return self.name
            case "d":
                return self.description
            case _:
                return super().__format__(format_spec)


@attrs.frozen
class NG911Field(yaml.YAMLObject):
    yaml_tag = "!Field"
    yaml_loader = yaml.SafeLoader
    yaml_dumper = ConfigDumper

    role: str
    name: str
    type: ArcFieldTypeStr
    priority: Literal["M", "C", "O", "T"]
    length: Optional[int] = None
    domain: Optional[NG911Domain] = None
    fill_value: Optional[Union[str, int, float]] = None

    @classmethod
    def from_yaml(cls, loader: yaml.SafeLoader, node: yaml.Node):
        # Explicitly writing this method was necessary when changing from @dataclass to @attrs.frozen
        assert isinstance(node, yaml.MappingNode)
        mapping = loader.construct_mapping(node, deep=True)
        return cls(**mapping)

    @property
    def arcpy_field(self) -> arcpy.Field:
        field = arcpy.Field()
        field.name = self.name
        field.type = self.type
        if self.length:
            field.length = self.length
        if self.domain:
            field.domain = self.domain.name
        return field

    @property
    def type_keyword(self) -> ArcFieldTypeKeyword:
        """
        Returns the keyword used in field creation based on the field's
        :attr:`type` attribute.

        .. seealso::

           :data:`FIELD_TYPE_KEYWORDS`
        """
        return FIELD_TYPE_KEYWORDS[self.type]

    @property
    def value_table_row(self) -> list[str | int | ArcFieldTypeKeyword | None]:
        """Returns a list representing the field's specifications, ordered as
        ``[<name>, <type>, <alias>, <length>, <default_value>, <domain>]``.
        Intended for use as an element of a ``list`` of ``list``\ s passed to
        ``arcpy.management.AddFields``."""
        return [self.name, self.type_keyword, None, self.length, None, self.domain.name if self.domain else None]

    @property
    def as_namespace(self) -> FieldInfoNamespace:
        return IterableNamespace(
            name=self.name,
            type=self.type,
            length=self.length,
            domain=self.domain.as_namespace if self.domain else None
        )

    def __format__(self, format_spec: str) -> str:
        """
        The following custom format specifiers are implemented:

        - ``r`` - Returns the :attr:`role` attribute
        - ``n`` - Returns the :attr:`name` attribute
        """
        # noinspection PyUnreachableCode
        match format_spec:
            case "r":
                return self.role
            case "n":
                return self.name
            case _:
                return super().__format__(format_spec)


@attrs.frozen(repr=False, str=False)
class NG911GeodatabaseInfo(yaml.YAMLObject):
    yaml_tag = "!GDBInfo"
    yaml_loader = yaml.SafeLoader
    yaml_dumper = ConfigDumper

    spatial_reference_factory_code_2d: int
    spatial_reference_factory_code_3d: int
    required_dataset_name: str
    optional_dataset_name: str


T_FieldNamespace = TypeVar("T_FieldNamespace", bound=IterableNamespace[NG911Field])


@attrs.frozen(repr=False, str=False)
class NG911FeatureClass(Generic[T_FieldNamespace]):
    role: str
    name: str
    geometry_type: Literal["POINT", "POLYLINE", "POLYGON"]
    dataset: str
    unique_id: NG911Field
    fields: T_FieldNamespace

    def __repr__(self):
        return f"<{__class__.__name__} '{self.role}'>"

    @property
    def field_value_table_rows(self) -> list[list[str | int | ArcFieldTypeKeyword | None]]:
        """
        Returns a ``list`` of ``list``\ s suitable for passing to
        ``arcpy.management.AddFields`` as the argument for
        ``field_description``.

        ..seealso::

           :attr:`NG911Field.value_table_row`
        """
        f: NG911Field
        return [f.value_table_row for f in self.fields.values()]

    @property
    def as_namespace(self) -> FeatureClassInfoNamespace:
        return IterableNamespace(
            name=self.name,
            geometry_type=self.geometry_type,
            fields=[*self.fields.keys()]
        )

    def __format__(self, format_spec: str) -> str:
        """
        The following custom format specifiers are implemented:

        - ``r`` - Returns the :attr:`role` attribute
        - ``n`` - Returns the :attr:`name` attribute
        """
        # noinspection PyUnreachableCode
        match format_spec:
            case "r":
                return self.role
            case "n":
                return self.name
            case _:
                return super().__format__(format_spec)


__all__ = ["NG911Domain", "NG911Field", "NG911FeatureClass", "NG911GeodatabaseInfo", "FeatureClassInfoNamespace", "FieldInfoNamespace", "DomainInfoNamespace", "CodedValueDict", "DomainType", "ConfigDumper", "WDManager", "YAMLIterableNamespaceMeta"]